﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using NDF.ConnectionLibrary;
using NDF.XnaLibrary;
using System.Runtime.Serialization;
using System.Net;
using NDF.XnaLibrary.Models;
using Microsoft.Xna.Framework;

namespace NDF.MainServerSide
{
    class Server
    {
		static TcpListener receivingServer;
		static TcpListener sendingServer;
		static List<PlayerInfo> players = new List<PlayerInfo>();
		static Dictionary<int, CharacterInfo> playerCharacters = new Dictionary<int, CharacterInfo>();
		static Queue<PlayerInfo> playersToBeRemoved = new Queue<PlayerInfo>();
		static int PlayerIndexer = 1;
		static int bufferSize = Parser.ClientInformationLength;

		//A few global variables to reduce the overhead of recreating strings and byte arrays in the networking methods.
		//Two of each, since sending and receiving are asynchronous and are likely happen at the same time.
		static string textToClient, textFromClient;
		static byte[] dataToClient, dataFromClient;

		//A simple flag to raise when the server should be restarted
		static bool RestartRequested = false;

        static void Main(string[] args)
        {
			TextReader file = new StreamReader("ServerSettings.txt");
			ServerSettings settings = Parser.ParseServerSettings(file);

			receivingServer = new TcpListener(new IPEndPoint(settings.IP, settings.ClientToServerPort));
			receivingServer.Start();
			sendingServer = new TcpListener(new IPEndPoint(settings.IP, settings.ServerToClientPort));
			sendingServer.Start();

			Console.WriteLine("Server started at {0}", DateTime.Now.ToShortTimeString());
			//Asynchronously wait for and accept new clients.
			Task.Factory.StartNew(WaitAcceptClients, TaskCreationOptions.LongRunning);
			//Asynchronously handle input
			Task.Factory.StartNew(HandleInput, TaskCreationOptions.LongRunning);
			
			try
			{
				while (true)
				{
					HandlePlayerData();
				}
			}
			finally
			{
				foreach (var player in players)
				{
					player.Close();
				}
				receivingServer.Stop();
				sendingServer.Stop();
			}
        }

		/// <summary>
		/// Handles input commands from the console.
		/// </summary>
		private static void HandleInput()
		{
			while (true)
			{
				string[] input = Console.ReadLine().Split(' ');
				switch (input[0])
				{
					case "help" :
						PrintHelp();
						break;
					case "all" :
						PrintConnectedPlayers();
						break;
					case "kick" :
						if (input.Length > 2)
							KickPlayer(input[1], input[2]);
						else
							KickPlayer(input[1], "Kicked");
						break;
					case "restart" :
						//Raise the flag. The restart will take place after the next HandlePlayerData cycle
						RestartRequested = true;
						break;
				}
			}
		}

		#region InputCommands
		
		private static void PrintHelp()
		{
			Console.WriteLine("Available commands:\n" + 
				"all				Shows all the currently connected clients.\n" + 
				"kick <ID> <Reason>	Kick the player	with the given ID and the specified reason.\n" + 
				"restart				Kicks all players, clears all buffers and streams.\n");
		}

		private static void PrintConnectedPlayers()
		{
			if (players.Count == 0)
				Console.WriteLine("No players are currently connected.");
			else
			{
				Console.WriteLine("ID:		IP:");
				foreach (var player in players)
				{
					Console.WriteLine("{0}		{1}", player.PlayerID, player.SendingClient.Client.RemoteEndPoint.ToString());
				}
			}
		}

		private static void Restart()
		{
			foreach (var player in players)
			{
				player.Close();
			}
			players.Clear();
			playerCharacters.Clear();
			playersToBeRemoved.Clear();
			PlayerIndexer = 1;
			Console.WriteLine("Server restarted at {0}", DateTime.Now.ToShortTimeString());
		}

		private static void KickPlayer(string id, string reason)
		{
			int playerID;
			bool parseResult = Int32.TryParse(id, out playerID);
			if (parseResult)
			{
				int index = players.FindIndex(player => player.PlayerID == playerID);
				if (index == -1)
				{
					Console.WriteLine("Invalid ID.");
					return;
				}
				if (reason.Length > Parser.MaxDisconnectedReasonLength)
				{
					Console.WriteLine("Reason must be less than {0} characters.", Parser.MaxDisconnectedReasonLength);
					return;
				}

				ServerNotification notification = Parser.CreateServerNotification(playerID, CharacterType.Player, "Disconnected: " + reason);
				SendNotification(notification);

				playersToBeRemoved.Enqueue(players[index]);
				Console.WriteLine("Player {0} kicked successfully.", playerID);

			}
			else
			{
				Console.WriteLine("You didnt enter a valid number.");
			}
		}

		#endregion

		/// <summary>
		/// Waits and accepts new clients in an infinite loop.
		/// </summary>
		private static void WaitAcceptClients()
		{
			while (true)
			{
				//Accept the player 
				TcpClient receivingClient = receivingServer.AcceptTcpClient();
				TcpClient sendingClient = sendingServer.AcceptTcpClient();

				//Send the newly connected player information about the already connected
				string characters = Parser.StringifyCharacterInfoDictionary(playerCharacters);
				byte[] data = Parser.EncodingFormat.GetBytes(characters);
				ConnectionHelper.SendData(sendingClient, data);

				//Add the player to the lists
				players.Add(new PlayerInfo(PlayerIndexer, sendingClient, receivingClient));
				playerCharacters.Add(PlayerIndexer, GetDefaultCharacter());

				//Send notifications to the already connected players
				ServerNotification notification = Parser.CreateServerNotification(PlayerIndexer, CharacterType.Player, "Connected");
				SendNotification(PlayerIndexer, notification);

				Console.WriteLine("Client from {0} accepted at {1}. ID: {2}",
					sendingClient.Client.RemoteEndPoint,
					DateTime.Now.ToShortTimeString(),
					PlayerIndexer);

				PlayerIndexer++;
			}			
		}

		/// <summary>
		/// Gets a new CharacterInfo object representing the default character state.
		/// </summary>
		private static CharacterInfo GetDefaultCharacter()
		{
			Random rng = new Random();
			CharacterInfo info = new CharacterInfo();
			info.State = CharacterState.Idle;
			info.Position = new Vector3(200, 1, 50);
			info.Rotation = Quaternion.Identity;
			info.WalkingDirection = Vector3.Zero;
			info.CastingActionInfo = new CastingActionInfo();
			info.CombatStats = new CombatStatistics();
			info.CombatStats.MovementSpeed = 1f;
			info.Inventory = new List<EquipmentInfo>();
			info.Inventory.Add(new EquipmentInfo() { Id = 1, Type = EquipmentType.OneHandedWeapon, Slot = EquipmentSlot.WeaponSlot1 });

			return info;
		}

		/// <summary>
		/// Handles any information waiting to be received by the clients.
		/// </summary>
		private static void HandlePlayerData()
		{  
			//Check whether there`s new data in the streams
			for (int i = 0; i < players.Count; i++)
			{
				try
				{
					if (players[i].ReceivingStream.DataAvailable)
					{
						//If so - read it, validate it, send it over to the rest of the clients
						dataFromClient = ConnectionHelper.ReadData(players[i].ReceivingStream, bufferSize);
						textFromClient = Parser.EncodingFormat.GetString(dataFromClient);
						ServerNotification notification = Parser.CreateServerNotification(players[i].PlayerID, CharacterType.Player, textFromClient.TrimEnd());
						
						//This method currently is nothing but a dummy one. Must change accordingly ASAP.
						UpdatePlayers(notification);
						SendNotification(players[i].PlayerID, notification);
					}
				}
				catch (Exception e)
				{
					CheckException(e, players[i].PlayerID);
				}
			}
			if (RestartRequested)
			{
				Restart();
				RestartRequested = false;
				return;
			}
			RemoveDisconnected();
		}

		/// <summary>
		/// Updates the information about the players based on the specified ServerNotification.
		/// </summary>
		/// <param name="notification">The notification to update according to.</param>
		private static void UpdatePlayers(ServerNotification notification)
		{
			switch(notification.NotificationType)
			{
				case NotificationType.Walking :
					playerCharacters[notification.ID].State = CharacterState.Moving;
					Vector3 position = (Vector3) notification.Data;
					if (position.LengthSquared() != 1)
						playerCharacters[notification.ID].Position = position;
					break;
				case NotificationType.Casting :
					playerCharacters[notification.ID].State = CharacterState.Casting;
					break;
				case NotificationType.Rotating :
					playerCharacters[notification.ID].Rotation = (Quaternion) notification.Data;
					break;
				case NotificationType.Disconnected:
					playersToBeRemoved.Enqueue(players.Find(x => x.PlayerID == notification.ID));
					Console.WriteLine("Client {0} disconnected. Reason: {1}", notification.ID, notification.Data);
					break;
			}
		}

		/// <summary>
		/// Sends a notification to all connected players but the one with the given ID. 
		/// </summary>
		/// <param name="IDtoSkip">The ID of the player to be skipped.</param>
		/// <param name="notification">The notification to be sent.</param>
		private static void SendNotification(int IDtoSkip, ServerNotification notification)
		{
			try
			{
				dataToClient = Parser.EncodingFormat.GetBytes(notification.ToString());

				for (int i = 0; i < players.Count; i++)
				{
					if (players[i].PlayerID == IDtoSkip)
						continue;
					ConnectionHelper.SendData(players[i].SendingStream, dataToClient);
				}
			}
			catch (Exception e)
			{
				CheckException(e);
			}
		}

		/// <summary>
		/// Sends a notification to all connected players.
		/// </summary>
		/// <param name="notification">The notification to be sent.</param>
		private static void SendNotification(ServerNotification notification)
		{
			try
			{
				dataToClient = Parser.EncodingFormat.GetBytes(notification.ToString());

				for (int i = 0; i < players.Count; i++)
				{
					ConnectionHelper.SendData(players[i].SendingStream, dataToClient);
				}
			}
			catch (Exception e)
			{
				CheckException(e);
			}
		}

		///<summary>
		///Checks if the exception is of type IO, Socket or ObjectDisposed, that is expected to happen when a user`s game crashes
		///The method queues the player for removal and sends notifications
		///</summary>
		///<param name="e">The exception to be tested.</param>
		///<param name="index">The INDEX of the player in the players list.</param>
		private static void CheckException(Exception e, int index)
		{
			if (e is IOException || e is ObjectDisposedException || e is SocketException)
			{
				ServerNotification notification = Parser.CreateServerNotification(players[index].PlayerID, CharacterType.Player,
					"Disconnected: Client crashed or terminated");
				SendNotification(players[index].PlayerID, notification);
				Console.WriteLine("Client {0} disconnected. Reason: {1}", notification.ID, notification.Data);
				playersToBeRemoved.Enqueue(players[index]);
			}
			else
				throw e;
		}

		/// <summary>
		/// Checks if the exception is of type IO, Socket or ObjectDisposed, that is expected to happen when a user`s game crashes
		/// </summary>
		private static void CheckException(Exception e)
		{
			if (e is IOException || e is ObjectDisposedException || e is SocketException)
			{
				//Expected
			}
			else
				throw e;
		}

		/// <summary>
		/// Removes all players queued for removal and clears the queue.
		/// </summary>
		private static void RemoveDisconnected()
		{
			for (int i = 0; i < playersToBeRemoved.Count; i++)
			{
				PlayerInfo player = playersToBeRemoved.Dequeue();
				players.Remove(player);
				playerCharacters.Remove(player.PlayerID);
				player.Close();
			}
		}
    }
}
